// 
//  Copyright © 2008, 2009 Jiří Zárevúcky <zarevucky.jiri@gmail.com>
// 
//  This program is free software: you can redistribute it and/or modify
//  it under the terms of the GNU Affero General Public License as
//  published by the Free Software Foundation, either version 3 of the
//  License, or (at your option) any later version.
// 
//  This program is distributed in the hope that it will be useful,
//  but WITHOUT ANY WARRANTY; without even the implied warranty of
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//  GNU Affero General Public License for more details.
// 
//  You should have received a copy of the GNU Affero General Public License
//  along with this program.  If not, see <http://www.gnu.org/licenses/>.
// 
// 


using System;
using System.Threading;
using System.Net.Sockets;
using System.Collections.Generic;

using Anculus.Core;
using Galaxium.Core;

using Galaxium.Protocol.Xmpp.Library.Xml;
using Galaxium.Protocol.Xmpp.Library.Core;
using Galaxium.Protocol.Xmpp.Library.Extensions;
using Galaxium.Protocol.Xmpp.Library.Extensions.Disco;
using Galaxium.Protocol.Xmpp.Library.Utility;
using Galaxium.Protocol.Xmpp.Library.Messaging;
using Galaxium.Protocol.Xmpp.Library.Helpers;

// TODO: make IDisposable

namespace Galaxium.Protocol.Xmpp.Library
{
	public abstract class Client: CoreStream, IPresenceListener, IMessageListener
	{
		public event EventHandler<ConvEventArgs> ConversationCreated;
		
		public event EventHandler<EntityEventArgs> EntityWhitelisted;
		public event EventHandler<EntityEventArgs> EntityBlacklisted;
		
		public event EventHandler<StatusChangeEventArgs> StatusChanged;
		public event EventHandler<PresenceEventArgs> SendingPresence;
		
		public event EventHandler<ExceptionEventArgs> ErrorOccured;
		
		public event EventHandler<AvatarChangeEventArgs> AvatarChanged;
		public event EventHandler<StanzaErrorEventArgs> AvatarChangeFailed;
		
		private Dictionary<JabberID, WeakReference<Conversation>> _conversations =
			new Dictionary<JabberID, WeakReference<Conversation>> ();
		
		private Info   _my_info;
		private Iq     _disco_response;
		private string _verification_string;
		private string _software_url;

		// list of own resources that don't support vCard-based avatar
		private HashSet<string> _nonavatar_resources = new HashSet<string> ();
		private string _avatar_checksum = null;
		private bool _advertising_avatar = false;
		
		private Dictionary<string, QueryHandler> _info_handlers =
			new Dictionary<string, QueryHandler> ();
		
		private Dictionary<JabberID, int> _presence_whitelist =
			new Dictionary<JabberID, int> ();
		
		private HashSet<JabberID> _query_whitelist =
			new HashSet<JabberID> ();
		
		private Presence _current_presence;
		
		#region Properties

		public Roster              Roster              { get; private set; }

		public MessageManager      MessageManager      { get; private set; }
		
		public Status CurrentStatus      { get; private set; }
		public string CurrentDescription { get; private set; }
		public sbyte? CurrentPriority    { get; private set; }
		
		public string SoftwareURL {
			get { return _software_url; }
			set {
				_software_url = value;
				ForceCapsUpdate ();
			}
		}
		
		public bool IsAvailable {
			get {
				return ConnectionState == ConnectionState.Connected &&
					CurrentStatus != Status.Offline;
			}
		}
		
		public ICacheHandler CacheHandler { get; private set; }
		
		public bool CacheEnabled {
			get { return CacheHandler != null; }
		}
		
		public Info ServerInfo { get; private set; }
		
		private ManualResetEvent _server_info_waiter = new ManualResetEvent (false);
		
		#endregion

		public Client (JabberID uid, ICacheHandler cache_handler)
			:base (uid)
		{
			CacheHandler = cache_handler;
			Roster = new Roster (this);
			MessageManager = new MessageManager (this);
			
			base.DefineGetQueryHandler (Namespaces.DiscoInfo, HandleInfoQuery);
			
			_my_info = new Info ();
			RegisterFeature (Namespaces.DiscoInfo);
			RegisterFeature (Namespaces.Capabilities);
			RegisterFeature (Namespaces.ChatStates);

			Roster.ContactRemoved += HandleContactRemoved;
			Roster.ContactUpdated +=  (s, e) => NotifyContactChange (e.Contact);
			
			base.AttachMessageListener (this, 0);
			base.AttachPresenceListener (this, 0);
			
			base.StreamError += (s, e) => OnErrorOccured (new StreamErrorException (e.Error));
			
			base.ConnectionStateChanged += HandleConnectionStateChanged;
			
			Roster.SubscriptionUpdated += (s, e) => {
				if (!e.Contact.HasSubscription
				    && _presence_whitelist.ContainsKey (e.Contact.Jid))
					UpdateDirectPresence (e.Contact.Jid);
				// make sure current conversants get the presence
				// if the subscription is removed mid-conversation
			};
			
			SendingPresence += AppendCapabilities;
			SendingPresence += AppendAvatarChecksum;
		}

		void HandleConnectionStateChanged (object sender, ConnectionStateEventArgs e)
		{
			if (e.State == ConnectionState.Connected) {
				Roster.RequestItems ();
				RequestServerInfo ();
			}
			
			if (e.State == ConnectionState.Disconnected) {
				CurrentStatus = Status.Offline;
				OnStatusChange (Status.Offline, null, null);
			}
		}
		
		void AppendCapabilities (object sender, PresenceEventArgs e)
		{
			if (_verification_string == null)
				GenerateVerificationString ();
			
			var caps = new Element ("c", Namespaces.Capabilities);
			caps ["hash"] = "sha-1";
			caps ["node"] = SoftwareURL;
			caps ["ver"] = _verification_string;
			e.Presence.AppendChild (caps);
		}
		
		void AppendAvatarChecksum (object sender, PresenceEventArgs e)
		{
			var x = new Element ("x", Namespaces.vCardUpdate);
			
			if (_avatar_checksum != null) {
				var photo = new Element ("photo");
				if (_avatar_checksum != String.Empty)
					photo.Text = _avatar_checksum;
				x.AppendChild (photo);
			}
			
			e.Presence.AppendChild (x);
		}

		
		bool IPresenceListener.HandlePresence (Presence pres)
		{
			if (pres.From == this.UID)
				OnStatusChange (StatusMethods.GetPresenceStatus (pres), pres.Status, pres.Priority);
			
			var uid = pres.From.Bare ();

			if (uid == this.UID.Bare ())
				HandleSelfPresence (pres);

			WeakReference<Conversation> reference;
			if (_conversations.TryGetValue (uid, out reference) && reference.IsAlive)
				reference.Target.HandlePresence (pres);
			
			return true;
		}

		private void HandleSelfPresence (Presence pres)
		{
			string checksum = null;
			
			if (pres.Type == PresType.Unavailable)
				_nonavatar_resources.Remove (pres.From.Resource);
			else {
				var avatar_ext = pres.FirstChild ("x", Namespaces.vCardUpdate);
				if (avatar_ext == null)
					_nonavatar_resources.Add (pres.From.Resource);
				else {
					_nonavatar_resources.Remove (pres.From.Resource);
					var elm = avatar_ext.FirstChild ("photo", null);
					if (elm != null)
						checksum = elm.Text ?? String.Empty;
				}
			}

			if (_nonavatar_resources.Count != 0 && _advertising_avatar) {
				_avatar_checksum = null;
				_advertising_avatar = false;
				UpdatePresence ();
			}
			else if (_nonavatar_resources.Count == 0 && !_advertising_avatar) {
				_avatar_checksum = null;
				_advertising_avatar = true;
				UpdatePresence ();
				UpdateAvatar ();
			}
			else if (_advertising_avatar && checksum != null && checksum != _avatar_checksum) {
				_avatar_checksum = null;
				UpdatePresence ();
				UpdateAvatar ();
			}
		}
		
		public void WaitForDisco ()
		{
			_server_info_waiter.WaitOne ();
		}
		
		private void RequestServerInfo ()
		{
			var query = new Iq (IqType.Get, Namespaces.DiscoInfo);
			query.To = UID.Domain;
			_server_info_waiter.Reset ();
			this.SendQuery (query, (result) => {
				if (result == null) {
					Log.Error ("Disconnected while retrieving server's disco");
				}
				else if (result.IsError) {
					Log.Error ("Couldn't retrieve server's disco information: "
					           + result.Error.GetHumanRepresentation ());
				}
				else {
					ServerInfo = new Info (result);
				}
				_server_info_waiter.Set ();
			});
		}

		private void UpdateAvatar ()
		{
			var vcard = new vCard (this, null);
			vcard.Finished += delegate(object sender, EventArgs e) {
				string checksum;
				byte[] data;
				if (String.IsNullOrEmpty (vcard.PhotoData)) {
					checksum = String.Empty;
					data = null;
				}
				else {
					try { data = Convert.FromBase64String (vcard.PhotoData); }
					catch { return; } // TODO: some better error handling
					checksum = Cryptography.Sha1HexHash (data);
				}
				
				if (AvatarChanged != null)
					AvatarChanged (this, new AvatarChangeEventArgs (null, vcard.PhotoType, data));
				
				if (_advertising_avatar) {
					_avatar_checksum = checksum;
					UpdatePresence ();
				}
			};
			vcard.Retrieve ();
		}
		
		bool IMessageListener.HandleMessage (XmlMessage msg)
		{			
			switch (msg.Type) {
				case MsgType.Normal:
				case MsgType.Headline:
					MessageManager.HandleMessage (msg);
					break;
				case MsgType.Chat:
					GetConversation (msg.From.Bare ()).HandleMessage (msg as ChatMessage);
					break;
				case MsgType.Error:
					switch ((msg.ID ?? "x").Substring (0, 1)) {
						case "n":
							MessageManager.HandleError (msg);
							break;
						case "c":
							GetConversation (msg.From.Bare ()).HandleError (msg);
							break;
					}
					break;
			}
			
			return true;
		}

		protected override bool CanQuery (JabberID jid)
		{
			if (jid == null) return true;
			var bare = jid.Bare ();
			if (bare == this.UID.Bare ()) return true; // from myself
			
			var contact = jid == null ? null : Roster.TryGetContact (jid);
			if (contact != null && contact.HasSubscription) return true; // subscribed contacts
			
			return _query_whitelist.Contains (jid.Bare ());
		}
		
		#region Conversations
		
		private Conversation StartConversation (JabberID jid)
		{
			jid = jid.Bare ();
			var conversation = new Conversation (Roster.GetContact (jid));
			_conversations [jid] = new WeakReference<Conversation> (conversation);
			OnConversationCreated (conversation);
			return conversation;
		}
		
		public Conversation GetConversation (JabberID jid)
		{
			jid = jid.Bare ();
			return (_conversations.ContainsKey (jid) && _conversations [jid].IsAlive) ?
				_conversations [jid].Target : StartConversation (jid);
		}

		internal void LockConversation (JabberID jid)
		{
			_conversations [jid.Bare ()].IsLocked = true;
		}

		internal void UnlockConversation (JabberID jid)
		{
			_conversations [jid.Bare ()].IsLocked = false;
		}
		
		private void NotifyContactChange (Contact contact)
		{
			WeakReference<Conversation> reference;
			if (_conversations.TryGetValue (contact.Jid, out reference) && reference.IsAlive)
			    reference.Target.HandleContactChange ();
		}
		
		private void HandleContactRemoved (object sender, ContactEventArgs e)
		{
			WeakReference<Conversation> reference;
			if (_conversations.TryGetValue (e.Contact.Jid, out reference) && reference.IsAlive)
				reference.Target.HandleContactRemoving ();
			_conversations.Remove (e.Contact.Jid);
		}
		
		#endregion

		#region Features & Capabilities
		
		private void HandleInfoQuery (Iq query)
		{
			var node = query.Query ["node"];

			QueryHandler handler;
			
			if (node == null || node == SoftwareURL + '#' + _verification_string) {
				if (_disco_response == null)
					GenerateDisco ();
				
				_disco_response.To = query.From;
				_disco_response.Query ["node"] = node;
				_disco_response.ID = query.ID;
				
				Send (_disco_response);
			}
			else if (_info_handlers.TryGetValue (node, out handler)) {
				handler (query);
			}
			else {
				Send (query.CreateErrorResponse ("item-not-found"));
			}
		}
		
		private void ForceCapsUpdate ()
		{
			_disco_response = null;
			_verification_string = null;
			UpdatePresence ();
		}
		
		public void RegisterInfoHandler (string node, QueryHandler handler)
		{
			_info_handlers.Add (node, handler);
		}
		
		public void UnregisterInfoHandler (string node)
		{
			_info_handlers.Remove (node);
		}
		
		private void GenerateDisco ()
		{
			_disco_response = _my_info.CreateResult (null, null, null);
		}
		
		private void GenerateVerificationString ()
		{
			_verification_string = _my_info.GenerateVerificationString ();
		}
		
		public void AddIdentity (string category, string type, string name, string lang)
		{
			AddIdentity (new Identity (category, type, name, lang));
		}
		
		public void AddIdentity (Identity identity)
		{
			_my_info.AddIdentity (identity);
			ForceCapsUpdate ();
		}
		
		public void RegisterFeature (string feature)
		{
			_my_info.AddFeature (feature);
			ForceCapsUpdate ();
		}
		
		public void UnregisterFeature (string feature)
		{
			_my_info.RemoveFeature (feature);
			ForceCapsUpdate ();
		}
		
		#endregion
		
		public void AddEntitySession (JabberID jid)
		{
			if (_presence_whitelist.ContainsKey (jid)) {
				_presence_whitelist [jid] ++;
			} else {
				Log.Debug ("Adding jid " + jid + "to the whitelist.");
				_presence_whitelist.Add (jid, 1);
				AddToWhitelist (jid);
				
				if (IsAvailable)
					UpdateDirectPresence (jid);
				
				if (EntityWhitelisted != null)
					EntityWhitelisted (this, new EntityEventArgs (jid));
			}
		}
		
		public void RemoveEntitySession (JabberID jid, bool send_unavailable)
		{
			if (!_presence_whitelist.ContainsKey (jid)) return;
			_presence_whitelist [jid] --;
			if (_presence_whitelist [jid] > 0) return;
			
			Log.Debug ("Removing jid " + jid + "from the whitelist.");
			_presence_whitelist.Remove (jid);
			RemoveFromWhitelist (jid);
			
			var contact = Roster.TryGetContact (jid);
			
			if (send_unavailable && IsAvailable && (contact == null || !contact.HasSubscription))
				Send (new Presence (PresType.Unavailable, jid));
			
			if (EntityBlacklisted != null)
				EntityBlacklisted (this, new EntityEventArgs (jid));
		}
		
		public void AddToWhitelist (JabberID jid)
		{
			_query_whitelist.Add (jid.Bare ());
		}
		
		public void RemoveFromWhitelist (JabberID jid)
		{
			_query_whitelist.Remove (jid.Bare ());
		}
		
		#region Status
		
		private object _status_lock = new object ();
		
		public void SetStatus (Status status, string description, sbyte? priority)
		{
			if (ConnectionState != ConnectionState.Connected)
				throw new InvalidOperationException ("You are not connected");
			
			CurrentStatus = status;
			CurrentDescription = description;
			CurrentPriority = priority;
			
			UpdatePresence ();
			OnStatusChange (Status.Updating, "Waiting for reply.", 0);
			
			if (status == Status.Offline) // HACK for ejabberd (?)
				OnStatusChange (status, description, 0);
		}
		
		public void UpdatePresence ()
		{
			if (ConnectionState != ConnectionState.Connected) return;
			var presence = StatusMethods.ToPresence (CurrentStatus, CurrentDescription, CurrentPriority);
			OnPresenceUpdate (presence);
		}

		private void OnStatusChange (Status status, string description, sbyte? priority)
		{
			if (StatusChanged == null) return;
			var args = new StatusChangeEventArgs (status, description, priority ?? 0);
			
			lock (_status_lock) {
				StatusChanged (this, args);
			}
		}
		
		private void OnPresenceUpdate (Presence presence)
		{
			if (SendingPresence != null)
				SendingPresence (this, new PresenceEventArgs (presence));

			_current_presence = presence;
			BroadcastPresence ();
		}
		
		private void BroadcastPresence ()
		{
			Send (_current_presence);
			
			if (_current_presence.Type != PresType.Unavailable) {
				foreach (var jid in _presence_whitelist.Keys) 
					UpdateDirectPresence (jid);
			}
		}
		
		private void UpdateDirectPresence (JabberID jid)
		{
			_current_presence.To = jid;
			Send (_current_presence);
			_current_presence.To = null;
		}
		
		#endregion
		
		#region Events
		
		protected void OnErrorOccured (Exception e)
		{
			if (ErrorOccured != null)
				ErrorOccured (this, new ExceptionEventArgs (e));
		}
		
		protected void OnConversationCreated (Conversation conversation)
		{
			if (ConversationCreated != null)
				ConversationCreated (this, new ConvEventArgs (conversation));
		}
		
		#endregion

		public void SetAvatar (string type, byte[] data)
		{
			ThrowUtility.ThrowIfNull ("data", data);
			
			var hash = Cryptography.Sha1HexHash (data);
			var base64 = Convert.ToBase64String (data);
			
			if (_avatar_checksum == hash) return;

			var vcard = new vCard (this, null);

			EventHandler finished;
			finished = (s, e) => {
				vcard.PhotoType = type;
				vcard.PhotoData = base64;
				vcard.Finished -= finished;
				vcard.Finished += (sd, a) => {
					_avatar_checksum = hash;
					_advertising_avatar = true;
					UpdatePresence ();
					if (AvatarChanged != null)
						AvatarChanged (this, new AvatarChangeEventArgs (null, type, data));
				};
				vcard.Set ();
			};
			
			vcard.Finished += finished;
			vcard.Failed += (s, e) => {
				if (AvatarChangeFailed != null)
					AvatarChangeFailed (this, new StanzaErrorEventArgs (e.Error));
			};
			vcard.Retrieve ();
		}
	}
}